package com.ebs.md.fxmonitor.orders;

/**
 * Perforce Revision:
 *  $File: //depot/MarketData/fxmonitor/main/src/main/java/com/ebs/md/fxmonitor/orders/TopOfBooksChangeListener.java $
 *  $Revision: #11 $  $Change: 114787 $  $DateTime: 2012/09/10 09:00:34 $  $Author: Arik.Sher $
 */

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.jms.JMSException;

import org.springframework.util.Assert;

import com.ebs.commons.comp.IParsedPayload;
import com.ebs.commons.comp.IPubSubChannel;
import com.ebs.commons.comp.ParsedPayload;
import com.ebs.md.api.BookSide;
import com.ebs.md.api.IDetailedBookEntry;
import com.ebs.md.api.IIdentifiedOrder;
import com.ebs.md.api.IOrderBook;
import com.ebs.md.api.IProduct;
import com.ebs.md.api.OrderBookListener;
import com.ebs.md.api.OrderSide;
import com.ebs.md.api.Rate;
import com.ebs.md.api.orderbook.BookEntryPosition;
import com.ebs.md.api.orderbook.BookEntryPositionOfDeleted;
import com.ebs.md.api.product.PairProduct;

/**
 * This OrderBookListener looks for TopOFBook changes.
 * 
 * it handle Multiple OrderBooks, by their Product.
 */
public class TopOfBooksChangeListener
implements          OrderBookListener<IOrderBook<IDetailedBookEntry>, IDetailedBookEntry, IIdentifiedOrder>
{
	protected final static String PAYLOAD_MQ_NAME = "TOBC"; // Top Of Book Change
	protected final static int    SIZE_OF_TOPS_LIST_ON_PAYLOAD = 4;
	
	private Map<PairProduct, Rate>  bidTopRateBeforeMap   = newMap();
	private Map<PairProduct, Rate>  offerTopRateBeforeMap = newMap();
	
	
	protected IPubSubChannel pubChannel;
	protected String         publishToTopicName;
	
	
	public void setPublishToTopicName(String publishToTopicName) {
		this.publishToTopicName = publishToTopicName;
	}

	public void setPubChannel(IPubSubChannel pubChannel) {
		this.pubChannel = pubChannel;
	}

	public void init() {
		Assert.notNull(pubChannel, "'pubChannel' cannot be null");
		Assert.hasText(publishToTopicName, "'publishToTopicName' should contain text");
	}
	
	private Map<PairProduct, Rate> newMap() {
		return new HashMap<PairProduct, Rate>(128);
	}
	
	protected <T> T safeGetFirst(Collection<T> coll) {
		if ( coll == null || coll.isEmpty() ) {
			return null;
		}
		
		return coll.iterator().next();
	}
	
	public void beforeModifyBookEntry(IOrderBook<IDetailedBookEntry> orderbook, IDetailedBookEntry bookEntry, IIdentifiedOrder order) {
		// do nothing
	}

	public void  afterModifyBookEntry(IOrderBook<IDetailedBookEntry> orderbook, IDetailedBookEntry bookEntry, IIdentifiedOrder order) {
		// do nothing
	}

	public void beforeNewBookEntry(IOrderBook<IDetailedBookEntry> orderbook, IDetailedBookEntry newBookEntry, IIdentifiedOrder order) {
		PairProduct product = extractProduct(orderbook);
		Rate   bidTopRate = safeGetFirst(orderbook.getTopBidRates  (1));
		Rate offerTopRate = safeGetFirst(orderbook.getTopOfferRates(1));
		  bidTopRateBeforeMap.put(product,   bidTopRate );
		offerTopRateBeforeMap.put(product, offerTopRate );
	}

	public void  afterNewBookEntry(IOrderBook<IDetailedBookEntry> orderbook, IDetailedBookEntry newBookEntry, IIdentifiedOrder order, BookEntryPosition position) {
		checkAfterOrder(orderbook, order);
	}

	protected void checkAfterOrder(IOrderBook<IDetailedBookEntry> orderbook, IIdentifiedOrder order) {
		PairProduct product = extractProduct(orderbook);
		Rate   bidTopRate = safeGetFirst(orderbook.getTopBidRates  (1));
		Rate offerTopRate = safeGetFirst(orderbook.getTopOfferRates(1));
		
		Rate prevBidTopRate   =   bidTopRateBeforeMap.get(product);
		Rate prevOfferTopRate = offerTopRateBeforeMap.get(product);
		
		boolean bidTopChanged   = !safeAreRatesEqual(bidTopRate,   prevBidTopRate);
		boolean offerTopChanged = !safeAreRatesEqual(offerTopRate, prevOfferTopRate);
		
		if ( bidTopChanged || offerTopChanged ) {
			IParsedPayload payload = createNotificationPayloadByOrder(orderbook, product, bidTopRate, offerTopRate, order, prevBidTopRate, prevOfferTopRate);

			notifyTOBChange(payload);
		}
	}

	protected void checkAfterSnapshot(IOrderBook<IDetailedBookEntry> orderbook, Calendar snapshotTime) {
		PairProduct product = extractProduct(orderbook);
		Rate   bidTopRate = safeGetFirst(orderbook.getTopBidRates  (1));
		Rate offerTopRate = safeGetFirst(orderbook.getTopOfferRates(1));
		
		Rate prevBidTopRate   =   bidTopRateBeforeMap.get(product);
		Rate prevOfferTopRate = offerTopRateBeforeMap.get(product);
		
		boolean bidTopChanged   = !safeAreRatesEqual(bidTopRate,   prevBidTopRate);
		boolean offerTopChanged = !safeAreRatesEqual(offerTopRate, prevOfferTopRate);
		
		if ( bidTopChanged || offerTopChanged ) {
			BookSide bs;
			if ( bidTopChanged ) {
				bs = BookSide.BID;
			} else {
				bs = BookSide.OFFER;
			}
			
			IParsedPayload payload = createNotificationPayloadBySnapshot(orderbook, product, bidTopRate, offerTopRate
					, snapshotTime
					, bs
					, prevBidTopRate, prevOfferTopRate
					);

			notifyTOBChange(payload);
		}
	}
	
	protected void notifyTOBChange(IParsedPayload payload) {
		try {
			pubChannel.submitMessage(publishToTopicName, payload);
		} catch (JMSException e) {
			//TODO: log it...
			throw new RuntimeException("unexpected when trying to publish TopOfBook change", e);
		}
	}
	
	public void beforeRemoveBookEntry(IOrderBook<IDetailedBookEntry> orderbook, IDetailedBookEntry bookEntry, IIdentifiedOrder order) {
		PairProduct product = extractProduct(orderbook);
		Rate   bidTopRate = safeGetFirst(orderbook.getTopBidRates  (1));
		Rate offerTopRate = safeGetFirst(orderbook.getTopOfferRates(1));
		  bidTopRateBeforeMap.put(product,   bidTopRate );
		offerTopRateBeforeMap.put(product, offerTopRate );
	}
	
	public void  afterRemoveBookEntry(IOrderBook<IDetailedBookEntry> orderbook, IDetailedBookEntry bookEntry, IIdentifiedOrder order, BookEntryPositionOfDeleted position) {
		checkAfterOrder(orderbook, order);
	}

	public void beforeSnapshot(IOrderBook<IDetailedBookEntry> orderbook, Calendar snapshotTime, boolean isFirst) {
		PairProduct product = extractProduct(orderbook);
		Rate   bidTopRate = safeGetFirst(orderbook.getTopBidRates  (1));
		Rate offerTopRate = safeGetFirst(orderbook.getTopOfferRates(1));
		  bidTopRateBeforeMap.put(product,   bidTopRate );
		offerTopRateBeforeMap.put(product, offerTopRate );
	}
	
	public void  afterSnapshot(IOrderBook<IDetailedBookEntry> orderbook, Calendar snapshotTime, boolean isFirst) {
		if ( !isFirst ) {
			checkAfterSnapshot(orderbook, snapshotTime);
		}
	}

	protected void displayAfter(String action, IProduct product, Rate bidRate, Rate offerRate) {
		System.out.println(String.format("%s of '%s' - after. top bid:'%s', top offer:'%s'.", action, product, bidRate, offerRate) );
	}
	
	protected void addRateField(IParsedPayload pp, Rate rate, String fieldName) {
		if ( rate != null ) {
			pp.addField(  fieldName + "_str",  rate.toString());
			pp.addField(  fieldName,           rate);
		} else {
			pp.addField(  fieldName + "_str",  null);
			pp.addField(  fieldName,           null);
		}
	}

	protected List<IDetailedBookEntry> duplicateWithClone(List<IDetailedBookEntry> beList) {
		List<IDetailedBookEntry> retList = new ArrayList<IDetailedBookEntry>(beList.size());
		
		for ( IDetailedBookEntry curr : beList ) {
			IDetailedBookEntry clonedBE;
			try {
				clonedBE = (IDetailedBookEntry)curr.clone();
			} catch (CloneNotSupportedException e) {
				throw new RuntimeException("unexpected!!!", e);
			} 
			retList.add(clonedBE);
		}
		
		return retList;
	}
	
	protected IParsedPayload createBasicNotificationPayload(IOrderBook<IDetailedBookEntry> orderbook, PairProduct pairProduct, OrderSide orderSide
			, Rate     bidTopRate, Rate     offerTopRate
			, Rate prevBidTopRate, Rate prevOfferTopRate) {
		IParsedPayload pp = new ParsedPayload();
		
		pp.addField("mq_msg_type",   PAYLOAD_MQ_NAME);
		
		pp.addField("pair_product",  pairProduct);
		pp.addField("base_ccy_iso",  pairProduct.getBaseCcyIso());
		pp.addField("local_ccy_iso", pairProduct.getLocalCcyIso());
		pp.addField("matching_type", pairProduct.getMatchingType());

		pp.addField("qte_type",      orderSide.getLegacyCode());
		
		if ( OrderSide.OFFER.equals(orderSide) ) {
			pp.addField("qte_type_name", "offer");
		} else {
			pp.addField("qte_type_name", "bid");
		}
		
		addRateField(pp,     bidTopRate,        "bid_top_rate");
		addRateField(pp, prevBidTopRate,   "prev_bid_top_rate");
		addRateField(pp,     offerTopRate,      "offer_top_rate");
		addRateField(pp, prevOfferTopRate, "prev_offer_top_rate");
		
		// the top_xxx_rates_lists currently are used by the OffMarketListener
		pp.addField(  "top_bid_rates_list", orderbook.getTopBidRates  (SIZE_OF_TOPS_LIST_ON_PAYLOAD));
		pp.addField("top_offer_rates_list", orderbook.getTopOfferRates(SIZE_OF_TOPS_LIST_ON_PAYLOAD));
		
		// using duplicates book entries, because their state might change, between the time they will be referenced,
		//  and the time that the book would change again.
		// the book_entries currently are used by the CrossMarketListener
		pp.addField("book_entries_of_top_bid"
				, duplicateWithClone(
					orderbook.getTopBidsAtRate  (bidTopRate,   99999))
				);
		pp.addField("book_entries_of_top_offer"
				, duplicateWithClone(
					orderbook.getTopOffersAtRate(offerTopRate, 99999)) 
				);
		
		return pp;
	}
	
	protected IParsedPayload createNotificationPayloadByOrder(IOrderBook<IDetailedBookEntry> orderbook, PairProduct pairProduct
			, Rate     bidTopRate, Rate     offerTopRate, IIdentifiedOrder order
			, Rate prevBidTopRate, Rate prevOfferTopRate)
	{
		IParsedPayload pp = createBasicNotificationPayload(orderbook, pairProduct, order.getSide(), bidTopRate, offerTopRate, prevBidTopRate, prevOfferTopRate);

		pp.addField("action_name",   order.getOperation().toString());
		pp.addField("action_time",   order.getTime().getTime());
		pp.addField("order_id",      order.getId());
		pp.addField("order_amount",  order.getAmount());
		addRateField(pp,             order.getRate()
				  , "order_rate");
		pp.addField("deal_code",     order.getDealCode());
		pp.addField("trader_id",     order.getTraderId());
		pp.addField("order_total_amount",  order.getTotalAmount());
		
		return pp;
	}

	protected IParsedPayload createNotificationPayloadBySnapshot(IOrderBook<IDetailedBookEntry> orderbook, PairProduct pairProduct
			, Rate     bidTopRate, Rate     offerTopRate, Calendar snapshotTime, BookSide bookSide
			, Rate prevBidTopRate, Rate prevOfferTopRate)
	{
		IParsedPayload pp = createBasicNotificationPayload(orderbook, pairProduct, bookSide.getOrderSide(), bidTopRate, offerTopRate, prevBidTopRate, prevOfferTopRate);
		
		pp.addField("action_name",   "SNAPSHOT");
		pp.addField("action_time",   snapshotTime.getTime());
		pp.addField("order_id",      String.valueOf(snapshotTime.getTimeInMillis()));
/*	
 * on purpose, do not add null fields, so no exception / log entry would occur
 * 
		pp.addField("order_amount",  null);
		addRateField(pp,             null
				  , "order_rate");
		pp.addField("deal_code",     null);
		pp.addField("trader_id",     null);
		pp.addField("order_total_amount",  null);
*/
		return pp;
	}
	
	protected boolean safeAreRatesEqual(Rate r1, Rate r2) {
		if ( r1 == null ) {
			if ( r2 == null ) {
				return true;
			} else {
				return false;
			}
		}
		return r1.equals(r2);
	}
	
	protected PairProduct extractProduct(IOrderBook<IDetailedBookEntry> orderbook) {
		//TODO: think of way without casting(maybe add generic to OrderBook)
		IProduct iproduct = orderbook.getProduct();
		return (PairProduct)iproduct;
	}
	
	
}
